iT邦幫忙

2024 iThome 鐵人賽

DAY 30
0
Python

Python 數值與數學模組介紹與應用系列 第 30

Day30.04.decimal — 十進制定點和浮點算術14

  • 分享至 

  • xImage
  •  

以下是一些用作工具函數的例程,它們演示了如何使用 Decimal 類別:

moneyfmt

Decimal 轉換為貨幣格式的字串。

def moneyfmt(value, places=2, curr='', sep=',', dp='.',
             pos='', neg='-', trailneg=''):
    """Convert Decimal to a money formatted string."""
    q = Decimal(10) ** -places  # 2 places --> '0.01'
    sign, digits, exp = value.quantize(q).as_tuple()
    result = []
    digits = list(map(str, digits))
    build, next = result.append, digits.pop
    if sign:
        build(trailneg)
    for i in range(places):
        build(next() if digits else '0')
    if places:
        build(dp)
    if not digits:
        build('0')
    i = 0
    while digits:
        build(next())
        i += 1
        if i == 3 and digits:
            i = 0
            build(sep)
    build(curr)
    build(neg if sign else pos)
    return ''.join(reversed(result))

pi

計算 Pi 到當前精度。

def pi():
    """Compute Pi to the current precision."""
    getcontext().prec += 2  # extra digits for intermediate steps
    three = Decimal(3)      # substitute "three=3.0" for regular floats
    lasts, t, s, n, na, d, da = 0, three, 3, 1, 0, 0, 24
    while s != lasts:
        lasts = s
        n, na = n + na, na + 8
        d, da = d + da, da + 32
        t = (t * n) / d
        s += t
    getcontext().prec -= 2
    return +s  # unary plus applies the new precision

exp

返回 ex 次方。結果類型與輸入類型匹配。

def exp(x):
    """Return e raised to the power of x."""
    getcontext().prec += 2
    i, lasts, s, fact, num = 0, 0, 1, 1, 1
    while s != lasts:
        lasts = s
        i += 1
        fact *= i
        num *= x
        s += num / fact
    getcontext().prec -= 2
    return +s

cos

返回 x 的餘弦值(以弧度為單位)。

def cos(x):
    """Return the cosine of x as measured in radians."""
    getcontext().prec += 2
    i, lasts, s, fact, num, sign = 0, 0, 1, 1, 1, 1
    while s != lasts:
        lasts = s
        i += 2
        fact *= i * (i - 1)
        num *= x * x
        sign *= -1
        s += num / fact * sign
    getcontext().prec -= 2
    return +s

sin

返回 x 的正弦值(以弧度為單位)。

def sin(x):
    """Return the sine of x as measured in radians."""
    getcontext().prec += 2
    i, lasts, s, fact, num, sign = 1, 0, x, 1, x, 1
    while s != lasts:
        lasts = s
        i += 2
        fact *= i * (i - 1)
        num *= x * x
        sign *= -1
        s += num / fact * sign
    getcontext().prec -= 2
    return +s

Decimal 常見問題

Q. 總是輸入 decimal.Decimal('1234.5') 是否過於笨拙?使用互動解釋器時有沒有最小化輸入量的方式?

A. 有些使用者會將 Decimal 建構器簡寫為一個字母:

>>> import decimal
>>> D = decimal.Decimal
>>> D('1.23') + D('3.45')
Decimal('4.68')

這樣可以減少輸入量並提高代碼的可讀性。

Q. 在有兩個十進位位的定點數應用中,有些輸入值有許多位,需要被捨入。另一些數字不應具有多餘位,需要驗證有效性。這種情況該用什麼方法?

A. 使用 quantize() 方法將數字捨入到固定的小數位數。如果設定了 Inexact 陷阱,它也適用於驗證有效性:

from decimal import Decimal, Context, Inexact

TWOPLACES = Decimal(10) ** -2  # same as Decimal('0.01')

# Round to two places
Decimal('3.214').quantize(TWOPLACES)
Decimal('3.21')

# Validate that a number does not exceed two places
Decimal('3.21').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Decimal('3.21')

Decimal('3.214').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Traceback (most recent call last):
   ...
Inexact: None

Q. 當我使用兩個有效位元的輸入時,我要如何在一個應用中保持有效位元不變?

A. 某些運算如與整數相加、相減和相乘會自動保留固定的小數位數。其他運算如相除和非整數相乘則需要額外的 quantize() 處理步驟:

a = Decimal('102.72')  # Initial fixed-point values
b = Decimal('3.17')
a + b  # Addition preserves fixed-point
Decimal('105.89')
a - b
Decimal('99.55')
a * 42  # So does integer multiplication
Decimal('4314.24')
(a * b).quantize(TWOPLACES)  # Must quantize non-integer multiplication
Decimal('325.62')
(b / a).quantize(TWOPLACES)  # And quantize division
Decimal('0.03')

為了方便,可以定義處理 quantize() 步驟的函數:

def mul(x, y, fp=TWOPLACES):
    return (x * y).quantize(fp)

def div(x, y, fp=TWOPLACES):
    return (x / y).quantize(fp)

mul(a, b)  # Automatically preserve fixed-point
Decimal('325.62')
div(b, a)
Decimal('0.03')

Q. 表示同一個值有許多方式。數字 200, 200.000, 2E2.02E+4 都具有相同的值但其精度不同。是否有辦法將它們轉換為一個可識別的規範值?

A. normalize() 方法可以將所有相等的值轉換為單一表示形式:

values = map(Decimal, '200 200.000 2E2 .02E+4'.split())
[v.normalize() for v in values]
[Decimal('2E+2'), Decimal('2E+2'), Decimal('2E+2'), Decimal('2E+2')]

Q. 計算中的捨入是在什麼時候發生的?

A. 捨入是在計算之後發生的。decimal 模組設計中,數字被視為精確的,並且不依賴於當前上下文來創建。它們可以具有比當前上下文更高的精確度。計算過程會使用精確的輸入,然後對計算結果應用捨入(或其他上下文操作):

from decimal import Decimal, getcontext

getcontext().prec = 5
pi = Decimal('3.1415926535')  # More than 5 digits
pi  # All digits are retained
Decimal('3.1415926535')

pi + 0  # Rounded after an addition
Decimal('3.1416')

pi - Decimal('0.00005')  # Subtract unrounded numbers, then round
Decimal('3.1415')

pi + 0 - Decimal('0.00005')  # Intermediate values are rounded
Decimal('3.1416')

Q. 有些十進制值總是被印為指數表示形式。是否有辦法得到一個非指數表示形式?

A. 對於某些數值,指數表示法是唯一的表示方式。若不必關心有效位數,則可以移除指數和末尾的零,但這樣會丟失有效位:

def remove_exponent(d):
    return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()

remove_exponent(Decimal('5E+3'))
Decimal('5000')

Q. 是否有辦法將一個普通浮點數轉換為 Decimal

A. 是的,任何二元浮點數都可以精確地表示為 Decimal 值,但完全精確的轉換可能需要比預期更高的精度:

import math
Decimal(math.pi)
Decimal('3.141592653589793115997963468544185161590576171875')

Q. 在複雜的計算中,我怎麼能保證不會得到由精度不足和捨去異常所導致的虛假結果?

A. 使用 decimal 模組可以輕鬆檢測結果。最佳實踐是使用更高的精度和不同的捨入模式重新計算。明顯不同的結果顯示存在精度不足、捨入模式問題、不符合條件的輸入或是結果不穩定的演算法。

Q. 我發現上下文精度的應用只針對運算結果而不針對輸入。在混合使用不同精度的值時有什麼需要注意的嗎?

A. 是的,所有值都被視為精確的,只有結果會被捨入。對於輸入而言,這意味著「所輸入即所得」。但如果忘記了輸入沒有被捨入,結果可能會看起來很奇怪:

getcontext().prec = 3
Decimal('3.104') + Decimal('2.104')
Decimal('5.21')

Decimal('3.104') + Decimal('0.000') + Decimal('2.104')
Decimal('5.20')

解決方案是提高精度或使用單目加法運算對輸入執行強制捨入:

getcontext().prec = 3
+Decimal('1.23456789')  # unary plus triggers rounding
Decimal('1.23')

# 使用 create_decimal() 方法在建立輸入時執行捨入
Context(prec=5, rounding=ROUND_DOWN).create_decimal('1.2345678')
Decimal('1.2345')

Q. CPython 實作對於龐大數字是否夠快速?

A. 是的。在 CPython 和 PyPy3 實作中,decimal 模組的 C/CFFI 版本集成了高速 libmpdec 函式庫來實現任意精度正確捨入的十進制浮點算術。libmpdec 會對中等大小的數字使用 Karatsuba 乘法,而對非常巨大的數字使用數字原理變換。

對於大數字算術,最便捷的方法是使用 prec 的最大值:

from decimal import Decimal, Context, MAX_PREC, MAX_EMAX, MIN_EMIN, setcontext

setcontext(Context(prec=MAX_PREC, Emax=MAX_EMAX, Emin=MIN_EMIN))
x = Decimal(2) ** 256
x / 128
Decimal('904625697166532776746648320380374280103671755200316906558262375061821325312')

對於精度不足的結果,在 64 位元平台上 MAX_PREC 的值可能過大,會導致記憶體不足:

Decimal(1) / 3
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
MemoryError

在具有超量分配的系統上,可以根據可用的 RAM 大小來調整 prec。假設有

8GB 的 RAM 並期望同時有 10 個操作數,每個最多使用 500MB:

import sys
from decimal import Context, Decimal, Inexact

# Maximum number of digits for a single operand using 500MB in 8-byte words
# with 19 digits per word (4-byte and 9 digits for the 32-bit build):
maxdigits = 19 * ((500 * 1024**2) // 8)

# Check that this works:
c = Context(prec=maxdigits, Emax=MAX_EMAX, Emin=MIN_EMIN)
c.traps[Inexact] = True
setcontext(c)

# Fill the available precision with nines:
x = Decimal(0).logical_invert() * 9
sys.getsizeof(x)
524288112
x + 2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  decimal.Inexact: [<class 'decimal.Inexact'>]

decimal 模組提供了靈活且高精度的十進制算術,尤其適合對精度要求極高的金融、科學及工程應用。透過合理設定上下文精度、捨入模式和陷阱,使用者可以有效避免精度不足或異常結果。結合一系列工具函數,decimal 不僅提升了計算的精確性,也讓複雜運算變得更具可讀性與可靠性。

這30天介紹了許多的有關於數學模組的介紹與應用,讓我又更知道程式函數的多樣,也更熟悉了Python這個語言。


上一篇
Day29.04.decimal — 十進制定點和浮點算術13
系列文
Python 數值與數學模組介紹與應用30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言